#
# Copyright (C) 2007, 2008 Andrew Resch <andrewresch@gmail.com>
#
# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
# the additional special exception to link portions of this program with the OpenSSL library.
# See LICENSE for more details.
#

import logging

from gi.repository import GObject, Gtk

from deluge.common import decode_bytes

from .common import cmp, load_pickled_state_file, save_pickled_state_file

log = logging.getLogger(__name__)


class ListViewColumnState:
    """Class used for saving/loading column state."""

    def __init__(self, name, position, width, visible, sort, sort_order):
        self.name = name
        self.position = position
        self.width = width
        self.visible = visible
        self.sort = sort
        self.sort_order = sort_order


class ListView:
    """ListView is used to make custom GtkTreeViews.  It supports the adding
    and removing of columns, creating a menu for a column toggle list and
    support for 'status_field's which are used while updating the columns data.
    """

    class ListViewColumn:
        """Holds information regarding a column in the ListView"""

        def __init__(self, name, column_indices):
            # Name is how a column is identified and is also the header
            self.name = name
            # Column_indices holds the indexes to the liststore_columns that
            # this column utilizes. It is stored as a list.
            self.column_indices = column_indices
            # Column is a reference to the GtkTreeViewColumn object
            self.column = None
            # This is the name of the status field that the column will query
            # the core for if an update is called.
            self.status_field = None
            # If column is 'hidden' then it will not be visible and will not
            # show up in any menu listing;  it cannot be shown ever.
            self.hidden = False
            # If this is set, it is used to sort the model
            self.sort_func = None
            self.sort_id = None
            # Values needed to update TreeViewColumns
            self.column_type = None
            self.renderer = None
            self.text_index = 0
            self.value_index = 0
            self.pixbuf_index = 0
            self.data_func = None

    class TreeviewColumn(Gtk.TreeViewColumn):
        """
        TreeViewColumn does not signal right-click events, and we need them
        This subclass is equivalent to TreeViewColumn, but it signals these events

        Most of the code of this class comes from Quod Libet (http://www.sacredchao.net/quodlibet)
        """

        __gsignals__ = {
            'button-press-event': (GObject.SignalFlags.RUN_LAST, None, (object,))
        }

        def __init__(self, title=None, cell_renderer=None, **args):
            """Constructor, see Gtk.TreeViewColumn"""
            Gtk.TreeViewColumn.__init__(self, title, cell_renderer, **args)
            label = Gtk.Label(label=title)
            self.set_widget(label)
            label.show()
            label.__realize = label.connect('realize', self.on_realize)
            self.title = title
            self.data_func = None
            self.data_func_data = None
            self.cell_renderer = None

        def on_realize(self, widget):
            widget.disconnect(widget.__realize)
            del widget.__realize
            button = widget.get_ancestor(Gtk.Button)
            if button is not None:
                button.connect('button-press-event', self.on_button_pressed)

        def on_button_pressed(self, widget, event):
            self.emit('button-press-event', event)

        def set_cell_data_func_attributes(self, cell_renderer, func, func_data=None):
            """Store the values to be set by set_cell_data_func"""
            self.data_func = func
            self.data_func_data = func_data
            self.cell_renderer = cell_renderer

        def set_visible(self, visible):
            Gtk.TreeViewColumn.set_visible(self, visible)
            if self.data_func:
                if not visible:
                    # Set data function to None to prevent unnecessary calls when column is hidden
                    self.set_cell_data_func(self.cell_renderer, None, func_data=None)
                else:
                    self.set_cell_data_func(
                        self.cell_renderer, self.data_func, self.data_func_data
                    )

        def set_col_attributes(self, renderer, add=True, **kw):
            if add is True:
                for attr, value in kw.items():
                    self.add_attribute(renderer, attr, value)
            else:
                self.set_attributes(renderer, **kw)

    def __init__(self, treeview_widget=None, state_file=None):
        log.debug('ListView initialized..')

        if treeview_widget is not None:
            # User supplied a treeview widget
            self.treeview = treeview_widget
        else:
            self.treeview = Gtk.TreeView()

        self.treeview.set_enable_search(True)
        self.treeview.set_search_equal_func(self.on_keypress_search_by_name, None)

        if state_file:
            self.load_state(state_file)

        self.liststore = None
        self.model_filter = None

        self.treeview.set_reorderable(False)
        self.treeview.set_rubber_banding(True)  # Enable mouse multi-row selection.
        self.treeview.get_selection().set_mode(Gtk.SelectionMode.MULTIPLE)

        # Dictionary of 'header' or 'name' to ListViewColumn object
        self.columns = {}
        # Column_index keeps track of the order of the visible columns.
        self.column_index = []
        # The column types for the list store.. this may have more entries than
        # visible columns due to some columns utilizing more than 1 liststore
        # column and some columns being hidden.
        self.liststore_columns = []
        # The GtkMenu that is created after every addition, removal or reorder
        self.menu = None
        # A list of menus that self.menu will be a submenu of everytime it is
        # created.
        self.checklist_menus = []

        # Store removed columns state.  This is needed for plugins that remove
        # their columns prior to having the state list saved on shutdown.
        self.removed_columns_state = []

        # Since gtk TreeModelSort doesn't do stable sort, remember last sort order so we can
        self.last_sort_order = {}
        self.unique_column_id = None
        self.default_sort_column_id = None

        # Create the model filter and column
        self.add_bool_column('filter', hidden=True)

    def create_model_filter(self):
        """create new filter-model
        must be called after listview.create_new_liststore
        """
        model_filter = self.liststore.filter_new()
        model_filter.set_visible_column(self.columns['filter'].column_indices[0])
        self.model_filter = Gtk.TreeModelSort(model=model_filter)
        self.model_filter.connect('sort-column-changed', self.on_model_sort_changed)
        self.model_filter.connect('row-inserted', self.on_model_row_inserted)
        self.treeview.set_model(self.model_filter)
        self.set_sort_functions()
        self.set_model_sort()

    def set_model_sort(self):
        column_state = self.get_sort_column_from_state()
        if column_state:
            self.treeview.get_model().set_sort_column_id(
                column_state.sort, column_state.sort_order
            )
        # Using the default sort column
        elif self.default_sort_column_id:
            self.model_filter.set_sort_column_id(
                self.default_sort_column_id, Gtk.SortType.ASCENDING
            )
        self.model_filter.set_default_sort_func(
            self.generic_sort_func, self.get_column_index(_('Added'))[0]
        )

    def get_sort_column_from_state(self):
        """Find the first (should only be one) state with sort enabled"""
        if self.state is None:
            return None
        for column_state in self.state:
            if column_state.sort is not None and column_state.sort > -1:
                return column_state
        return None

    def on_model_sort_changed(self, model):
        if self.unique_column_id:
            self.last_sort_order = {}

            def record_position(model, path, _iter, data):
                unique_id = model[_iter][self.unique_column_id]
                self.last_sort_order[unique_id] = int(str(path))

            model.foreach(record_position, None)

    def on_model_row_inserted(self, model, path, _iter):
        if self.unique_column_id:
            self.last_sort_order.setdefault(
                model[_iter][self.unique_column_id], len(model) - 1
            )

    def stabilize_sort_func(self, sort_func):
        def stabilized(model, iter1, iter2, data):
            result = sort_func(model, iter1, iter2, data)
            if result == 0 and self.unique_column_id:
                unique1 = model[iter1][self.unique_column_id]
                unique2 = model[iter2][self.unique_column_id]
                if unique1 in self.last_sort_order and unique2 in self.last_sort_order:
                    result = cmp(
                        self.last_sort_order[unique1], self.last_sort_order[unique2]
                    )
                # If all else fails, fall back to sorting by unique column
                if result == 0:
                    result = cmp(unique1, unique2)

            return result

        return stabilized

    def generic_sort_func(self, model, iter1, iter2, data):
        return cmp(model[iter1][data], model[iter2][data])

    def set_sort_functions(self):
        for column in self.columns.values():
            sort_func = column.sort_func or self.generic_sort_func
            self.model_filter.set_sort_func(
                column.sort_id, self.stabilize_sort_func(sort_func), column.sort_id
            )

    def create_column_state(self, column, position=None):
        if not position:
            # Find the position
            for index, c in enumerate(self.treeview.get_columns()):
                if column.get_title() == c.get_title():
                    position = index
                    break
        sort = None
        if self.model_filter:
            sort_id, order = self.model_filter.get_sort_column_id()
            col_title = decode_bytes(column.get_title())
            if self.get_column_name(sort_id) == col_title:
                sort = sort_id

        return ListViewColumnState(
            column.get_title(),
            position,
            column.get_width(),
            column.get_visible(),
            sort,
            int(column.get_sort_order()),
        )

    def save_state(self, filename):
        """Saves the listview state (column positions and visibility) to
        filename."""
        # A list of ListViewColumnStates
        state = []

        # Workaround for all zero widths after removing column on shutdown
        if not any(c.get_width() for c in self.treeview.get_columns()):
            return

        # Get the list of TreeViewColumns from the TreeView
        for counter, column in enumerate(self.treeview.get_columns()):
            # Append a new column state to the state list
            state.append(self.create_column_state(column, counter))

        state += self.removed_columns_state

        self.state = state
        save_pickled_state_file(filename, state)

    def load_state(self, filename):
        """Load the listview state from filename."""
        self.state = load_pickled_state_file(filename)

    def set_treeview(self, treeview_widget):
        """Set the treeview widget that this listview uses."""
        self.treeview = treeview_widget
        self.treeview.set_model(self.liststore)
        return

    def get_column_index(self, name):
        """Get the liststore column indices belonging to this column.
        Will return a list.
        """
        return self.columns[name].column_indices

    def get_column_name(self, index):
        """Get the header name for a liststore column index"""
        for key, value in self.columns.items():
            if index in value.column_indices:
                return key

    def get_state_field_column(self, field):
        """Returns the column number for the state field"""
        for column in self.columns:
            if self.columns[column].status_field is None:
                continue

            for f in self.columns[column].status_field:
                if field == f:
                    return self.columns[column].column_indices[
                        self.columns[column].status_field.index(f)
                    ]

    def on_menuitem_toggled(self, widget):
        """Callback for the generated column menuitems."""
        # Get the column name from the widget
        name = widget.get_child().get_text()

        # Set the column's visibility based on the widgets active state
        try:
            self.columns[name].column.set_visible(widget.get_active())
        except KeyError:
            self.columns[decode_bytes(name)].column.set_visible(widget.get_active())
        return

    def on_treeview_header_right_clicked(self, column, event):
        if event.button == 3:
            self.menu.popup(None, None, None, None, event.button, event.get_time())

    def register_checklist_menu(self, menu):
        """Register a checklist menu with the listview.  It will automatically
        attach any new checklist menu it makes to this menu.
        """
        self.checklist_menus.append(menu)

    def create_checklist_menu(self):
        """Creates a menu used for toggling the display of columns."""
        menu = self.menu = Gtk.Menu()
        # Iterate through the column_index list to preserve order
        for name in self.column_index:
            column = self.columns[name]
            # If the column is hidden, then we do not want to show it in the
            # menu.
            if column.hidden is True:
                continue
            menuitem = Gtk.CheckMenuItem.new_with_label(column.name)
            # If the column is currently visible, make sure it's set active
            # (or checked) in the menu.
            if column.column.get_visible() is True:
                menuitem.set_active(True)
            # Connect to the 'toggled' event
            menuitem.connect('toggled', self.on_menuitem_toggled)
            # Add the new checkmenuitem to the menu
            menu.append(menuitem)

        # Attach this new menu to all the checklist_menus
        for _menu in self.checklist_menus:
            _menu.set_submenu(menu)
            _menu.show_all()
        return menu

    def create_new_liststore(self):
        """Creates a new GtkListStore based on the liststore_columns list"""
        # Create a new liststore with added column and move the data from the
        # old one to the new one.
        new_list = Gtk.ListStore(*tuple(self.liststore_columns))

        # This function is used in the liststore.foreach method with user_data
        # being the new liststore and the columns list
        def copy_row(model, path, row, user_data):
            new_list, columns = user_data
            new_row = new_list.append()
            for column in range(len(columns)):
                # Get the current value of the column for this row
                value = model.get_value(row, column)
                # Set the value of this row and column in the new liststore
                new_list.set_value(new_row, column, value)

        # Do the actual row copy
        if self.liststore is not None:
            self.liststore.foreach(copy_row, (new_list, self.columns))

        self.liststore = new_list

    def update_treeview_column(self, header, add=True):
        """Update TreeViewColumn based on ListView column mappings"""
        column = self.columns[header]
        tree_column = self.columns[header].column

        if column.column_type == 'text':
            if add:
                tree_column.pack_start(column.renderer, True)
            tree_column.set_col_attributes(
                column.renderer, add=add, text=column.column_indices[column.text_index]
            )
        elif column.column_type == 'bool':
            if add:
                tree_column.pack_start(column.renderer, True)
            tree_column.set_col_attributes(
                column.renderer, active=column.column_indices[0]
            )
        elif column.column_type == 'func':
            if add:
                tree_column.pack_start(column.renderer, True)
            indice_arg = column.column_indices[0]
            if len(column.column_indices) > 1:
                indice_arg = tuple(column.column_indices)
            tree_column.set_cell_data_func(
                column.renderer, column.data_func, indice_arg
            )
        elif column.column_type == 'progress':
            if add:
                tree_column.pack_start(column.renderer, True)
            if column.data_func is None:
                tree_column.set_col_attributes(
                    column.renderer,
                    add=add,
                    text=column.column_indices[column.text_index],
                    value=column.column_indices[column.value_index],
                )
            else:
                tree_column.set_cell_data_func(
                    column.renderer, column.data_func, tuple(column.column_indices)
                )
        elif column.column_type == 'texticon':
            if add:
                tree_column.pack_start(column.renderer[column.pixbuf_index], False)
                tree_column.pack_start(column.renderer[column.text_index], True)
            tree_column.set_col_attributes(
                column.renderer[column.text_index],
                add=add,
                text=column.column_indices[column.text_index],
            )
            if column.data_func is not None:
                tree_column.set_cell_data_func(
                    column.renderer[column.pixbuf_index],
                    column.data_func,
                    column.column_indices[column.pixbuf_index],
                )
        return True

    def remove_column(self, header):
        """Removes the column with the name 'header' from the listview"""
        # Store a copy of this columns state in case it's re-added
        state = self.create_column_state(self.columns[header].column)
        self.removed_columns_state.append(state)
        # Only remove column if column is associated with the treeview. This avoids
        # warning on shutdown when GTKUI is closed before plugins try to remove columns
        if self.columns[header].column.get_tree_view() is not None:
            self.treeview.remove_column(self.columns[header].column)
        # Get the column indices
        column_indices = self.columns[header].column_indices
        # Delete the column
        del self.columns[header]
        self.column_index.remove(header)
        # Shift the column_indices values of those columns affected by the
        # removal. Any column_indices > the one removed.
        for column in self.columns.values():
            if column.column_indices[0] > column_indices[0]:
                # We need to shift this column_indices
                for i, index in enumerate(column.column_indices):
                    column.column_indices[i] = index - len(column_indices)
                # Update the associated TreeViewColumn
                self.update_treeview_column(column.name, add=False)

        # Remove from the liststore columns list
        for index in sorted(column_indices, reverse=True):
            del self.liststore_columns[index]

        # Create a new liststore
        self.create_new_liststore()
        # Create new model for the treeview
        self.create_model_filter()

        # Re-create the menu
        self.create_checklist_menu()
        return

    def add_column(
        self,
        header,
        render,
        col_types,
        hidden,
        position,
        status_field,
        sortid,
        text=0,
        value=0,
        pixbuf=0,
        function=None,
        column_type=None,
        sort_func=None,
        tooltip=None,
        default=True,
        unique=False,
        default_sort=False,
    ):
        """Adds a column to the ListView"""
        # Add the column types to liststore_columns
        column_indices = []
        if isinstance(col_types, list):
            for col_type in col_types:
                self.liststore_columns.append(col_type)
                column_indices.append(len(self.liststore_columns) - 1)
        else:
            self.liststore_columns.append(col_types)
            column_indices.append(len(self.liststore_columns) - 1)

        # Add to the index list so we know the order of the visible columns.
        if position is not None:
            self.column_index.insert(position, header)
        else:
            self.column_index.append(header)

        # Create a new column object and add it to the list
        column = self.TreeviewColumn(header)
        self.columns[header] = self.ListViewColumn(header, column_indices)
        self.columns[header].column = column
        self.columns[header].status_field = status_field
        self.columns[header].sort_func = sort_func
        self.columns[header].sort_id = column_indices[sortid]
        # Store creation details
        self.columns[header].column_type = column_type
        self.columns[header].renderer = render
        self.columns[header].text_index = text
        self.columns[header].value_index = value
        self.columns[header].pixbuf_index = pixbuf
        self.columns[header].data_func = function

        if unique:
            self.unique_column_id = column_indices[sortid]
        if default_sort:
            self.default_sort_column_id = column_indices[sortid]

        # Create a new list with the added column
        self.create_new_liststore()

        # Happens only on columns added after the torrent list has been loaded
        if self.model_filter:
            self.create_model_filter()

        if column_type is None:
            return

        self.update_treeview_column(header)

        column.set_sort_column_id(self.columns[header].column_indices[sortid])
        column.set_clickable(True)
        column.set_resizable(True)
        column.set_expand(False)
        column.set_min_width(20)
        column.set_reorderable(True)
        column.set_visible(not hidden)
        column.connect('button-press-event', self.on_treeview_header_right_clicked)

        if tooltip:
            column.get_widget().set_tooltip_markup(tooltip)

        # Check for loaded state and apply
        column_in_state = False
        if self.state is not None:
            for column_state in self.state:
                if header == decode_bytes(column_state.name):
                    # We found a loaded state
                    column_in_state = True
                    if column_state.width > 0:
                        column.set_sizing(Gtk.TreeViewColumnSizing.FIXED)
                        column.set_fixed_width(column_state.width)

                    column.set_visible(column_state.visible)
                    position = column_state.position
                    break

        # Set this column to not visible if its not in the state and
        # its not supposed to be shown by default
        if not column_in_state and not default and not hidden:
            column.set_visible(False)

        if position is not None:
            self.treeview.insert_column(column, position)
        else:
            self.treeview.append_column(column)

        # Set hidden in the column
        self.columns[header].hidden = hidden
        self.columns[header].column = column
        # Re-create the menu item because of the new column
        self.create_checklist_menu()

        return True

    def add_text_column(
        self,
        header,
        col_type=str,
        hidden=False,
        position=None,
        status_field=None,
        sortid=0,
        column_type='text',
        sort_func=None,
        tooltip=None,
        default=True,
        unique=False,
        default_sort=False,
    ):
        """Add a text column to the listview.  Only the header name is required."""
        render = Gtk.CellRendererText()
        self.add_column(
            header,
            render,
            col_type,
            hidden,
            position,
            status_field,
            sortid,
            column_type=column_type,
            sort_func=sort_func,
            tooltip=tooltip,
            default=default,
            unique=unique,
            default_sort=default_sort,
        )

        return True

    def add_bool_column(
        self,
        header,
        col_type=bool,
        hidden=False,
        position=None,
        status_field=None,
        sortid=0,
        column_type='bool',
        tooltip=None,
        default=True,
    ):
        """Add a bool column to the listview"""
        render = Gtk.CellRendererToggle()
        self.add_column(
            header,
            render,
            col_type,
            hidden,
            position,
            status_field,
            sortid,
            column_type=column_type,
            tooltip=tooltip,
            default=default,
        )

    def add_func_column(
        self,
        header,
        function,
        col_types,
        sortid=0,
        hidden=False,
        position=None,
        status_field=None,
        column_type='func',
        sort_func=None,
        tooltip=None,
        default=True,
    ):
        """Add a function column to the listview.  Need a header name, the
        function and the column types."""

        render = Gtk.CellRendererText()
        self.add_column(
            header,
            render,
            col_types,
            hidden,
            position,
            status_field,
            sortid,
            column_type=column_type,
            function=function,
            sort_func=sort_func,
            tooltip=tooltip,
            default=default,
        )

        return True

    def add_progress_column(
        self,
        header,
        col_types=None,
        sortid=0,
        hidden=False,
        position=None,
        status_field=None,
        function=None,
        column_type='progress',
        tooltip=None,
        sort_func=None,
        default=True,
    ):
        """Add a progress column to the listview."""

        if col_types is None:
            col_types = [float, str]
        render = Gtk.CellRendererProgress()
        self.add_column(
            header,
            render,
            col_types,
            hidden,
            position,
            status_field,
            sortid,
            function=function,
            column_type=column_type,
            value=0,
            text=1,
            tooltip=tooltip,
            sort_func=sort_func,
            default=default,
        )

        return True

    def add_texticon_column(
        self,
        header,
        col_types=None,
        sortid=1,
        hidden=False,
        position=None,
        status_field=None,
        column_type='texticon',
        function=None,
        sort_func=None,
        tooltip=None,
        default=True,
        default_sort=False,
    ):
        """Adds a texticon column to the listview."""
        if col_types is None:
            col_types = [str, str]
        render1 = Gtk.CellRendererPixbuf()
        render2 = Gtk.CellRendererText()

        self.add_column(
            header,
            (render1, render2),
            col_types,
            hidden,
            position,
            status_field,
            sortid,
            column_type=column_type,
            function=function,
            pixbuf=0,
            text=1,
            tooltip=tooltip,
            sort_func=sort_func,
            default=default,
            default_sort=default_sort,
        )

        return True

    def on_keypress_search_by_name(self, model, column, key, _iter, *search_data):
        torrent_name_col = self.columns[_('Name')].column_indices[1]
        return not model[_iter][torrent_name_col].lower().startswith(key.lower())

    def restore_columns_order_from_state(self):
        if self.state is None:
            # No state file exists, so, no reordering can be done
            return
        columns = self.treeview.get_columns()

        def find_column(header):
            for column in columns:
                if column.get_title() == header:
                    return column

        restored_columns = []
        for col_state in self.state:
            if col_state.name in restored_columns:
                # Duplicate column in state!?!?!?
                continue
            elif not col_state.visible:
                # Column is not visible, no need to reposition
                continue

            try:
                column_at_position = columns[col_state.position]
            except IndexError:
                # Ignore extra columns from Plugins in col_state
                continue
            if col_state.name == column_at_position.get_title():
                # It's in the right position
                continue
            column = find_column(col_state.name)
            if not column:
                log.debug(
                    'Could not find column matching "%s" on state.', col_state.name
                )
                # The cases where I've found that the column could not be found
                # is when not using the english locale, ie, the default one, or
                # when changing locales between runs.
                # On the next load, all should be fine
                continue
            self.treeview.move_column_after(column, column_at_position)
            # Get columns again to keep reordering since positions have changed
            columns = self.treeview.get_columns()
            restored_columns.append(col_state.name)
        self.create_new_liststore()
